﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using log4net;
using System.Net;
using System.Net.Sockets;
using Repo7.BusinessLogic.Utils;

namespace Repo7.BusinessLogic.Network.Commander
{
    /// <summary>
    /// Repository Commander
    /// </summary>
    public class RepoCommander
    {
        #region Private Members

        /// <summary>
        /// Logger
        /// </summary>
        protected static readonly ILog logger = LogManager.GetLogger(typeof(RepoCommander));

        /// <summary>
        /// Default block size
        /// </summary>
        public static int DefaultBacklogSize = 1024;

        /// <summary>
        /// Server Thread
        /// </summary>
        private Thread ServerThread;

        #endregion

        #region Public Properties

        /// <summary>
        /// Thread signal.
        /// </summary>
        public static ManualResetEvent AllDoneThreadSignal;

        /// <summary>
        /// Backlog Size
        /// </summary>
        public int BacklogSize;

        /// <summary>
        /// Action to be performed when the message is received
        /// </summary>
        public static Action<RepoCommand> OnMessageArrived;

        #endregion

        #region Constructor

        /// <summary>
        /// Constructor
        /// </summary>
        public RepoCommander(Action<RepoCommand> onMessageArrived, int? backlogSize)
        {
            AllDoneThreadSignal = new ManualResetEvent(false);

            //Set the backlog size
            if (!backlogSize.HasValue)
            {
                BacklogSize = DefaultBacklogSize;
            }
            else
            {
                BacklogSize = backlogSize.Value;
            }

            OnMessageArrived = onMessageArrived;
        }

        #endregion

        #region Public Methods

        public void StartListening(IPEndPoint commandEndPoint)
        {
            //Create a TCP/IP socket.
            Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            // Bind the socket to the local endpoint and listen for incoming connections.

            try
            {
                listener.Bind(commandEndPoint);
                listener.Listen(BacklogSize);
                ServerThread = new Thread(new ParameterizedThreadStart(AcceptConnectionsRoutine));
                ServerThread.Start(listener);
            }
            catch (Exception exception)
            {
                logger.Fatal(Messages.ErrorBindingSocketToLocalEndpoint, exception);
            }
        }

        /// <summary>
        /// Abort server thread
        /// </summary>
        public void StopServer()
        {
            ServerThread.Abort();
        }

        /// <summary>
        /// Send a command to a Node specified by its end point
        /// </summary>
        /// <param name="destinationEndPoint">Node End point</param>
        /// <param name="command">Command</param>
        public void SendCommand(RepoCommand command, IPEndPoint endPointDestination, int tryCount)
        {
            int timeout = 1000;
            byte[] message = command.GetBytes();

            using (TcpClient client = new TcpClient())
            {
                while (tryCount > 0)
                {
                    try
                    {

                        client.Connect(endPointDestination);
                        var stream = client.GetStream();
                        stream.Write(message, 0, message.Length);

                        return;

                    }
                    catch (Exception exception)
                    {
                        logger.Error(Messages.RepoCommanderCouldNotSendCommand, exception);
                        --tryCount;
                    }
                    Thread.Sleep(timeout);
                }
            }
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Accept connections routine
        /// </summary>
        /// <param name="listener"></param>
        private void AcceptConnectionsRoutine(object listener)
        {
            try
            {
                while (true)
                {
                    // Set the event no signaled state
                    AllDoneThreadSignal.Reset();

                    // Start an asynchronous socket to listen for connections.
                    logger.Info(Messages.RepoCServerStarted);
                    (listener as Socket).BeginAccept(new AsyncCallback(AcceptCallback), listener);

                    //Wait for a connection to be made
                    AllDoneThreadSignal.WaitOne();
                }
            }
            finally
            {
                (listener as Socket).Close();
                logger.Info(Messages.RepoCServerStopped);
            }
        }

        /// <summary>
        /// Accept Socket Connection Callback
        /// </summary>
        /// <param name="asyncResult"></param>
        private static void AcceptCallback(IAsyncResult asyncResult)
        {
            // Signal the main thread to continue
            AllDoneThreadSignal.Set();

            // Get the socket that handles the client request.
            Socket listener = (Socket)asyncResult.AsyncState;
            Socket handler = listener.EndAccept(asyncResult);

            //Create the state object
            AsyncCommandTransferStateObject stateObject = new AsyncCommandTransferStateObject(handler);
            handler.BeginReceive(stateObject.Buffer, 0, AsyncCommandTransferStateObject.DefaultBufferSize, SocketFlags.None,
                new AsyncCallback(ReadCallback), stateObject);

        }

        private static void ReadCallback(IAsyncResult asyncResult)
        {
            //Retrieve the state object and the handler socket
            AsyncCommandTransferStateObject stateObject = (AsyncCommandTransferStateObject)asyncResult.AsyncState;

            Socket handlerSocket = stateObject.WorkSocket;

            //Read data from the client socket
            int bytesRead = handlerSocket.EndReceive(asyncResult);

            if (bytesRead > 0)
            {
                int offset = 0;
                //If message length does not have the size set, read it from the first bytes
                if (!stateObject.MessageLength.HasValue)
                {
                    stateObject.ReadMessageLength();
                    offset = sizeof(long);
                }

                //Store the data received so far
                stateObject.WriteBytesToMemory(offset, bytesRead);

                //Test to see if the message is read
                if (!stateObject.IsMessageReceived())
                {
                    handlerSocket.BeginReceive(stateObject.Buffer, 0, AsyncCommandTransferStateObject.DefaultBufferSize, SocketFlags.None,
                        new AsyncCallback(ReadCallback), stateObject);
                }
                else
                {
                    RepoCommand command = stateObject.DeserializeCommandFromBuffer();
                    RepoCommander.OnMessageArrived(command);
                }
            }
        }

        #endregion
    }
}
